## metakit_database.py - Module around the metakit database
## Copyright (C) January 26, 2005 Erick P. Bodine
## email: erick_bodine@comcast.net
## webaddress

## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License
## as published by the Free Software Foundation; either version 2
## of the License, or any later version.

## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.

## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

# TODO: Research into wether having a logs & tasks cache is more effecient than a lookup
# NOTE: Always do storage('file', 1) to create the file if it doesn't exist
# NOTE: We always do a db.getas() to return a view
# NOTE: Strings coming in from Unicode wxPython must be stringified before used by metakit
#       ie. projectName = str(projectName)

"""
NAME
metakit_database.py

SYNOPSIS


DESCRIPTION
You MUST do a db.getas() FIRST, before you can retrieve any information!

Since metakit is an in memory database, it is unnecessary to open & close
the database with each transaction like you would with another sql database.

AUTHOR
Erick P Bodine
"""

import os
import sys
import pprint
import logging
import metakit

import FocusExceptions

__author__ = "$Author: ebodine $"
__date__ = "$Date: 2005-05-10 22:39:18 -0600 (Tue, 10 May 2005) $"
__version__ = "$Revision: 45 $"

log = logging.getLogger("myfocus")

PROJECT_VIEW = "projects[name:S,task_view:S,log_view:S]"
TASK_VIEW = "_tasks[title:S,project:S,comments:S,eta:S,startDate:S,endDate:S]"
LOG_VIEW = "_logs[date:S,body:S]"


class MetakitBase:
    def __init__(self, databaseFile):
        self.storage = None
        self.databaseFile = databaseFile
        log.debug("metakit version: %s" % metakit.version)
    
    def _getView(self, view):
        """ Return a view (cursor)
        Params:
            view = The view you want to retrieve
        """
        if not self.storage:
            self.storage = metakit.storage(self.databaseFile, 1)
        return self.storage.getas(view)
        
    def _computeEstimatedTime(self, startDate, endDate):
        """ Returns the ETA for a task
        If the end_date is empty just return a '+', also able to return a
        negative number.
        """
        result = '0'
        if endDate == "" or endDate == None:
            return result
        return result


class MetakitDatabase(MetakitBase):
    def __init__(self, databaseFile):
        MetakitBase.__init__(self, databaseFile)
        self.tasksCache = {}
    
    #-- Project methods ---#
    def createProject(self, projectName):
        """ Create a new project
        Params:
            projectName = Name of the project to create
        """
        projectName = str(projectName)
        log.debug("Creating project: %s" % projectName)
        
        projectView = self._getView(PROJECT_VIEW)
        if projectView.find(name=projectName) != -1:
            raise FocusExceptions.DuplicateTableName, \
                    "Already another project named: (%s)" % projectName
            
        projectView.append(name=projectName, task_view=projectName+"_tasks", 
                            log_view=projectName+"_logs")
        self.storage.getas(projectName+TASK_VIEW)
        self.storage.getas(projectName+LOG_VIEW)
        self.storage.commit()
        
    def renameProject(self, oldName, newName):
        """ Rename the passed in project. You not only have to change 
        the project view name, but you must also change the tasks and 
        logs view names
        Params:
            oldName = The old project name
            newName = The new project name
        """
        oldName = str(oldName)
        newName = str(newName)
        log.debug("Renaming %s to %s" % (oldName, newName))
        
        projectView = self._getView(PROJECT_VIEW)
        oldProjectIndex = projectView.find(name=oldName)
        
        if oldProjectIndex != -1:
            projectView.append(name=newName, task_view=newName+"_tasks", log_view=newName+"_logs")
            self.__changeLogTaskViews(projectView[oldProjectIndex].task_view, 
                                            oldName+TASK_VIEW, newName)
            self.__changeLogTaskViews(projectView[oldProjectIndex].log_view, oldName+LOG_VIEW, newName)
            projectView.delete(oldProjectIndex)
            self.storage.commit()
            
        else:
            log.error("Invalid project name (%s)" % oldName)
    
    def deleteProject(self, projectName):
        """ Delete the passed in project
        Params:
            projectName = Name of the project to delete
        """
        projectName = str(projectName)
        log.debug("deleting project: %s" % projectName)

        projectView = self._getView(PROJECT_VIEW)
        projectIndex = projectView.find(name=projectName)
        
        # In order to delete the project we also have to remove the task
        #  & log views assoc. w/ the project
        if projectIndex != -1:
            for t in (projectView[projectIndex].task_view, projectView[projectIndex].log_view):
                self.storage.getas(t)
                
            projectView.delete(projectIndex)
            self.storage.commit()
        else:
            log.error("Invalid project name (%s)" % projectName)
        
    def getProjects(self):
        """ Get a list of all the projects
        Returns:
            a list of projects
        """
        projectView = self._getView(PROJECT_VIEW)
        return [ r.name for r in projectView ]
        
    def getCurrentProject(self):
        """ Return the current project.
        Returns:
            a string containing the first project or empty string if 
            there aren't any projects
        """
        projects = self.getProjects()
        if projects:
            return projects[0]
        return ""
        
    #-- Task methods --#
    def addTask(self, projectName, newTask, comments="", startDate="", endDate=None):
        """ Add the passed task
        Updates the tasks_cache as well.
        Params:
            projectName = The name of the project the task is associated with
            newTask = The name of the task to be added
        """
        newTask = str(newTask)
        projectName = str(projectName)
        log.debug("adding task \"%s\" to project\"%s\"" % (newTask, projectName))

        tasksView = self._getView(projectName+TASK_VIEW)
        tasksView.append(title=newTask, project=projectName, comments=comments, eta='0', 
                            startDate=startDate, endDate=endDate)
        self.storage.commit()
        
        idx = len(self.tasksCache)
        self.tasksCache[newTask] = [str(idx), newTask, projectName, comments, '0', startDate, endDate]
                                    
    def deleteTask(self, projectName, task):
        """ Delete the passed in task
        Params:
            projectName = The name of the project to delete from
            task = The name of the task to delete
        """
        task = str(task)
        projectName = str(projectName)
        log.debug("Deleting task \"%s\" from project \"%s\"" % (task, projectName))
        
        taskView = self._getView(projectName+TASK_VIEW)
        taskIndex = taskView.find(title=task)
        
        if taskIndex != -1:
            taskView.delete(taskIndex)
            self.storage.commit()
        else:
            log.error("Can't delete task, invalid task title: %s" % task)
        
    def getTaskInfo(self, projectName, taskName):
        """ Get the row info for the passed in task name
        Params:
            projectName = The name of the project the task is associated with
            taskName = The name of the task we want the info of
        Returns:
            a list of task information
            (title, projectName, comments, eta, startDate, endDate)
        """
        taskName = str(taskName)
        projectName = str(projectName)
        log.debug("getting taskInfo from task %s for project %s" % (taskName, projectName))
        
        strResults = []
        tasksView = self._getView(projectName+TASK_VIEW)
        
        results = tasksView.select(title=taskName)
        
        # TODO: Figure out how to get <view>.select results in a more elegant manner
        
        r = results[0]
        strResults.append(r.title)
        strResults.append(r.project)
        strResults.append(r.comments)
        strResults.append(r.eta)
        strResults.append(r.startDate)
        strResults.append(r.endDate)
        
        return strResults
        
    def setTaskInfo(self, projectName, taskName):
        """ Set task information.
        Params:
            projectName = The project to retrieve tasks from
            taskName = The task to set information for
        """
        taskName = str(taskName)
        projectName = str(projectName)
        log.debug("Setting task information for task \"%s\" in project \"%s\"" % (taskName, projectName))
        
        # TODO: Complete the setTaskInfo() method
        
    def getProjectTasks(self, projectName):
        """ Retrieve the list of tasks from the task view. The task view
        is passed project + TASK_VIEW
        Params:
            projectName = The project to retrieve tasks from
        Returns:
            a list of all project tasks
        """
        projectName = str(projectName)
        log.debug("Getting tasks for project \"%s\"" % projectName)
        
        tasks = []
        self.tasksCache = {}   # Empty out the cache each time
        taskView = self._getView(projectName+TASK_VIEW)
        
        for i,r in enumerate(taskView):
            startDate = r.startDate
            endDate = r.endDate
            eta = self._computeEstimatedTime(startDate, endDate)
            self.tasksCache[r.title] = [str(i), r.title, r.project, r.comments, eta, startDate, endDate]
            tasks.append(r.title)
        
        return tasks
        
    def getAllTasks(self):
        """ Return the tasks for all projects
        Returns:
            a dictionary of all tasks
                allTasks = { 1: [1, 'eta', 'project', 'task'] }
        """
        index = 0
        projectView = self._getView(PROJECT_VIEW)
        
        for p in projectView:
            taskViewName = p.task_view
            log.debug("taskViewName: %s" % taskViewName)
            tv = self._getView(taskViewName+TASK_VIEW)
            for t in tv:
                index += 1
                allTasks[index] = [index, t.eta, t.project, t.task]
                
        return allTasks
        
        
    #-- Log methods --#
    def addLog(self, projectName, newLog, bodyText):
        """ Add the new log
        Params:
            projectName = The name of the project to add to 
            newLog = The log (date string) to add
        """
        newLog = str(newLog)
        projectName = str(projectName)
        log.debug("adding log \"%s\" to project \"%s\"" % (newLog, projectName))
        
        self.logView = self._getView(projectName+LOG_VIEW)
        self.logView.append(date=newLog, body=bodyText)
        self.storage.commit()
        
    def deleteLog(self, projectName, mfLog):
        """ Delete the passed log. 
        Params:
            projectName = The name of the project to add to
            mfLog = The log to delete
        """
        mfLog = str(mfLog)
        projectName = str(projectName)
        log.debug("Deleting log \"%s\" from project \"%s\"" % (mfLog, projectName))
        
        logView = self._getView(projectName+LOG_VIEW)
        logIndex = logView.find(date=mfLog)
        
        if logIndex != -1:
            logView.delete(logIndex)
            self.storage.commit()
        else:
            log.error("Can't delete log, invalid log date: %s" % mfLog)
            
    def changeLog(self, projectName, oldLog, newLog):
        """ Change the date of the selected log
        Params:
            projectName = The name of the project containing the log
            oldLog = The old log date
            newLog = The new log date
        """
        oldLog = str(oldLog)
        newLog = str(newLog)
        projectName = str(projectName)
        log.debug("Changing log \"%s\" to \"%s\" in project \"%s\"" % (oldLog, newLog, projectName))
        
    def getLogInfo(self, projectName, mfLog):
        """ Bet the log information.
        Params:
            projectName = The name of the project that contains the log
            mfLog = The log whose information you want
        """
        mfLog = str(mfLog)
        projectName = str(projectName)
        log.debug("Getting info from log \"%s\" for project \"%s\"" % (mfLog, projectName))
        
        strResults = []
        logView = self._getView(projectName+LOG_VIEW)
        
        results = logView.select(date=mfLog)
        
        r = results[0]
        
        strResults.append(r.date)
        strResults.append(r.body)
        
        return strResults
        
    def getProjectLogs(self, projectName):
        """ Retrieve the list of logs from the log view.
        Params:
            projectName = The project to retrieve logs from
        """
        projectName = str(projectName)
        log.debug("Getting logs for project \"%s\"" % projectName)
        
        logs = []
        self.logsCache = {}
        logView = self._getView(projectName+LOG_VIEW)
        
        for i,r in enumerate(logView):
            self.logsCache[r.date] = [str(i), r.date, r.body]
            logs.append(r.date)
            
        return logs
        
    #--------------------------------------------------------------------#
    #                       Private methods                              #
    #--------------------------------------------------------------------#
    
    def __changeLogTaskViews(self, plView, oldView, newProjectName):
        """ Change all of the log & task views
        Params:
            plView = The description for a task or log view
            oldView = The description for the old task or log view
            newProjectName = The name of the new project
        """
        newProjectName = newProjectName+"_"+plView.split('_')[1]
        view = self.storage.view(plView)
        clone = self.__cloneView(view, newProjectName)
        assert view.properties() == clone.properties()
        assert len(view) == len(clone)
        self.storage.getas(oldView) # Delete the old view
        self.storage.commit()
    
    def __cloneView(self, view, newName):
        """ create a new named view which is a copy of the contents of
        the view.  Cannot contain any subviews.
        This is a slightly modified version of Brian Kelley's cloneView()
        function from the metakit mailing list.
        Params:
            view = Name of the task or logs view to clone
            newName = The name of the new project
        Returns:
            the new task or log view
        """
        structure = []
        properties = view.properties().keys()
        
        for p in view.structure():
            if p.type == "V":
                raise ValueError("View contains subviews, cannot clone")
            structure.append("%s:%s" % (p.name, p.type))
            
        newDescription = "%s[%s]" % (newName, ",".join(structure))
        newView = self.storage.getas(newDescription)
        
        # Populate the newView w/ data from the old view
        for i, row in enumerate(view):
            values = {}
            for p in properties:
                values[p] = getattr(row, p)
                
            newView.append(values)
            self.storage.commit()
        
        return newView


if __name__ == '__main__':
    
    import pprint
    
    DEBUG = False
    DATABASEFILE = "metakit_test.mk"
    
    os.remove(DATABASEFILE)
        
    print "Testing metakit_backend.py"
    print "database file: ", DATABASEFILE
    
    
    Backend = MetakitDatabase(DATABASEFILE)
    
    print "Creating projects..."
    project1 = "project1"
    project2 = "project2"
    
    Backend.createProject(project1)
    Backend.createProject(project2)
    
    print "getProjects(): "
    pprint.pprint(Backend.getProjects())
    
    print "adding tasks "
    Backend.addTask(project1, "project1 task1")
    Backend.addTask(project2, "project2 task1")
    Backend.addTask(project2, "project2 task2")
    Backend.addTask(project2, "project2 task3")
    
    print "%s getProjectTasks() : " % project1
    pprint.pprint(Backend.getProjectTasks(project1))
    print "%s getProjectTasks() : " % project2
    pprint.pprint(Backend.getProjectTasks(project2))
    
    print "getTaskInfo(): "
    pprint.pprint(Backend.getTaskInfo(project1, "project1 task1"))
    
    
#    fb.task_vw.append(title='add a task', comments='a wxfocus task test',
#                                start_date='3-13-2004', end_date='3-20-2004')
#    fb.task_vw.append(title='add another task', comments='a wxfocus task test2',
#                                start_date='3-13-2004', end_date='3-24-2004')
#    fb.task_vw.append(title='This is task3', comments='a wxfocus task test2',
#                                start_date='3-13-2004', end_date='4-4-2004')
#    fb.db.commit()
#    for r in fb.task_vw:
#        print r.title, r.comments, r.start_date, r.end_date
#
#    fb.log_vw = fb.db.getas(LOG_VIEW)
#    fb.log_vw.append(date='3-13-2004', body='This is a multiline\nbody comment')
#    fb.log_vw.append(date='3-14-2004', body='This is a multiline\nbody comment')
#    fb.db.commit()
#    
#    for r in fb.log_vw:
#        print r.date, r.body
